package org.codefilarete.stalactite.sql.result;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.reflection.Mutator;
import org.codefilarete.stalactite.sql.statement.binder.ResultSetReader;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.danekja.java.util.function.serializable.SerializableSupplier;

/**
 * @param <C> produced bean type
 * @param <I> the type of bean keys (input)
 * @author Guillaume Mary
 */
public interface ResultSetTransformer<C, I> {
	
	/**
	 * Defines a complementary column that will be mapped on a bean property.
	 * Null values will be passed to the consumer, hence the property mapper must be "null-value proof".
	 *
	 * @param columnConsumer the object that will do the reading and mapping
	 */
	<O> ResultSetTransformer<C, I> add(ColumnConsumer<C, O> columnConsumer);
	
	/**
	 * Detailed version of {@link #add(ColumnConsumer)}
	 *
	 * @param columnName the column name of the property source
	 * @param reader the object that helps to read the column
	 * @param combiner the applyer of the value over a bean property
	 * @param <V> the type of the read value, must be compatible with the bean property input
	 */
	default <V> void add(String columnName, ResultSetReader<V> reader, Mutator<C, V> combiner) {
		add(new ColumnConsumer<>(columnName, reader, combiner));
	}
	
	/**
	 * Specialized version of add(..) for a collection-typed property : will instantiate and set the collection before adding the
	 * {@link ResultSet} value. Be aware that this method focuses on filling a simple-typed {@link Collection} (Integer, String, ...), not
	 * complex-typed ones because {@link ResultSetReader} is only capable of reading one column. 
	 * 
	 * @param columnName the column name of the property source
	 * @param reader the object that helps to read the column
	 * @param collectionAccessor the collection getter
	 * @param collectionMutator the collection setter (called only if getter returns null)
	 * @param collectionFactory the collection factory (called only if getter returns null)
	 * @param <V> the type of the read value, must be compatible with the bean property input
	 * @param <U> the collection type
	 */
	default <V, U extends Collection<V>> void add(String columnName,
												  ResultSetReader<V> reader,
												  Function<C, U> collectionAccessor,
												  BiConsumer<C, U> collectionMutator,
												  Supplier<U> collectionFactory) {
		add(new ColumnConsumer<>(columnName, reader, (c, v) -> {
			U collection = collectionAccessor.apply(c);
			if (collection == null) {
				collection = collectionFactory.get();
				collectionMutator.accept(c, collection);
			}
			collection.add(v);
		}));
	}
	
	/**
	 * Converts the current {@link ResultSet} row into a bean.
	 * Depending on implementation, this can return a brand new instance, or a cached one (if the bean key is already known for instance).
	 * 
	 * @param resultSet not null
	 * @return an instance of T, newly created or not according to implementation
	 * @throws SQLException due to {@link ResultSet} reading
	 */
	C transform(ResultSet resultSet) throws SQLException;

	/**
	 * Clones this instance for another type of bean.
	 * Useful to map a bean inheriting from another because it avoids redeclaration of common column mapping. Then after cloning current instance,
	 * one has only to register specific columns of the inheriting bean.
	 * 
	 * @param beanType the target bean type
	 * @param beanFactory the adhoc constructor for the target bean
	 * @param <T> the target bean type
	 * @return a new instance, kind of clone of this but for another type
	 */
	<T extends C> ResultSetTransformer<T, I> copyFor(Class<T> beanType, SerializableFunction<I, T> beanFactory);
	
	/**
	 * Clones this instance for another type of bean.
	 * Useful to map a bean inheriting from another because it avoids redeclaration of common column mapping. Then after cloning current instance,
	 * one has only to register specific columns of the inheriting bean.
	 *
	 * @param beanType the target bean type
	 * @param beanFactory the adhoc constructor for the target bean
	 * @param <T> the target bean type
	 * @return a new instance, kind of clone of this but for another type
	 */
	<T extends C> ResultSetTransformer<T, I> copyFor(Class<T> beanType, SerializableSupplier<T> beanFactory);
	
	/**
	 * Will combine bean created by this instance with the one created by relatedBeanCreator thanks to given combiner.
	 *
	 * @param combiner the associative function between bean created by this instance and the one created by given transformer
	 * @param relatedBeanCreator creator of another type of bean that will be combined with the one created by this instance
	 * @param <K> other bean type
	 * @param <V> other bean key type
	 * @return this
	 */
	<K, V> ResultSetTransformer<C, I> add(BeanRelationFixer<C, V> combiner, ResultSetRowTransformer<V, K> relatedBeanCreator);
}
